// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/* eslint-disable @typescript-eslint/no-explicit-any */

function createDecorator(mapFn: (fn: Function, key: string) => Function): Function {
    return (_target: any, key: string, descriptor: any) => {
        let fnKey: string | null = null;
        let fn: Function | null = null;

        if (typeof descriptor.value === 'function') {
            fnKey = 'value';
            fn = descriptor.value;
        } else if (typeof descriptor.get === 'function') {
            fnKey = 'get';
            fn = descriptor.get;
        }

        if (!fn) {
            throw new Error('not supported');
        }

        descriptor[fnKey!] = mapFn(fn, key);
    };
}

export function memoize(_target: any, key: string, descriptor: any) {
    let fnKey: string | null = null;
    let fn: Function | null = null;

    if (typeof descriptor.value === 'function') {
        fnKey = 'value';
        fn = descriptor.value;

        if (fn!.length !== 0) {
            console.warn('Memoize should only be used in functions with zero parameters');
        }
    } else if (typeof descriptor.get === 'function') {
        fnKey = 'get';
        fn = descriptor.get;
    }

    if (!fn) {
        throw new Error('not supported');
    }

    const memoizeKey = `$memoize$${key}`;
    descriptor[fnKey!] = function (...args: any[]) {
        if (!this.hasOwnProperty(memoizeKey)) {
            Object.defineProperty(this, memoizeKey, {
                configurable: false,
                enumerable: false,
                writable: false,
                value: fn!.apply(this, args)
            });
        }

        return this[memoizeKey];
    };
}

export interface IDebounceReducer<T> {
    (previousValue: T, ...args: any[]): T;
}

export function debounce<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
    return createDecorator((fn, key) => {
        const timerKey = `$debounce$${key}`;
        const resultKey = `$debounce$result$${key}`;

        return function (this: any, ...args: any[]) {
            if (!this[resultKey]) {
                this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
            }

            clearTimeout(this[timerKey]);

            if (reducer) {
                this[resultKey] = reducer(this[resultKey], ...args);
                args = [this[resultKey]];
            }

            this[timerKey] = setTimeout(() => {
                fn.apply(this, args);
                this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
            }, delay);
        };
    });
}

export function throttle<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
    return createDecorator((fn, key) => {
        const timerKey = `$throttle$timer$${key}`;
        const resultKey = `$throttle$result$${key}`;
        const lastRunKey = `$throttle$lastRun$${key}`;
        const pendingKey = `$throttle$pending$${key}`;

        return function (this: any, ...args: any[]) {
            if (!this[resultKey]) {
                this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
            }
            if (this[lastRunKey] === null || this[lastRunKey] === undefined) {
                this[lastRunKey] = -Number.MAX_VALUE;
            }

            if (reducer) {
                this[resultKey] = reducer(this[resultKey], ...args);
            }

            if (this[pendingKey]) {
                return;
            }

            const nextTime = this[lastRunKey] + delay;
            if (nextTime <= Date.now()) {
                this[lastRunKey] = Date.now();
                fn.apply(this, [this[resultKey]]);
                this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
            } else {
                this[pendingKey] = true;
                this[timerKey] = setTimeout(() => {
                    this[pendingKey] = false;
                    this[lastRunKey] = Date.now();
                    fn.apply(this, [this[resultKey]]);
                    this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
                }, nextTime - Date.now());
            }
        };
    });
}
